package ar.uba.fi.tonyvaliente.utils.io;

import java.io.IOException;
import java.io.OutputStream;
import java.util.BitSet;


/**
 * Representa un stream de salida de bits que pueden
 * escribirse en cualquier <code>java.io.OutputStream</code>.
 * Dado que los <code>OutputStream</code>s operan unicamente
 * con bytes, los objetos de la presente clase requieren
 * que se indique una forma de rellenar el ultimo byte,
 * la cual esta dada por el enumerado <code>ByteCompletionMode</code>.
 * @author dmorello
 */
public class OutputBitstream {

	private OutputStream stream;
	private ByteCompletionMode completionMode;
	private byte lastByte;
	private byte bitsInLastByte;
	
	/**
	 * Crea una instancia de la clase asociada a un <code>OutputStream</code>
	 * con una forma de relleno del ultimo byte dada por un
	 * <code>ByteCompletionMode</code>. 
	 * @param stream <code>OutputStream</code> en el cual escribir los bits
	 * @param completionMode modo de relleno del ultimo byte de <code>stream</code>
	 */
	public OutputBitstream(OutputStream stream, ByteCompletionMode completionMode) {
		this.stream = stream;
		this.completionMode = completionMode;
		this.lastByte = 0;
		this.bitsInLastByte = 0;
	}
	
	/**
	 * Cierra el bitstream y lo deja inhabilitado para nuevas escrituras.
	 * Al hacerlo, completa el ultimo byte escrito segun el modo de relleno
	 * especificado en el constructor. Si los bits escritos alcanzan a rellenar
	 * ese ultimo byte, se escribe uno mas segun esa forma de relleno.
	 * @throws IOException si ocurre un error al cerrar
	 */
	public void close() throws IOException {
		completeByte();
		stream.close();
		completionMode = null;
	}
	
	/**
	 * Escribe un bit en el bitstream
	 * @param bit bit a escribir
	 * @throws IOException si ocurre algun error al escribir
	 */
	public void write(boolean bit) throws IOException {
		lastByte <<= 1;
		if (bit) {
			lastByte++;
		}
		if (++bitsInLastByte == 8) {
			stream.write(lastByte);
			lastByte = 0;
			bitsInLastByte = 0;
		}
	}
	
	/**
	 * Escribe varios bits en el bitstream
	 * @param bits array de <code>boolean</code> con los bits a escribir
	 * @throws IOException si ocurre algun error al escribir
	 */
	public void write(boolean[] bits) throws IOException {
		for (boolean bit: bits) {
			write(bit);
		}
	}
	
	/**
	 * Escribe en el bitstream los bits de un <code>BitSet</code>
	 * @param bitSet <code>BitSet</code> con los bits a escribir
	 * @throws IOException si ocurre algun error al escribir
	 */
	public void write(BitSet bitSet) throws IOException {
		final int size = bitSet.size();
		for (int i = 0; i < size; i++) {
			boolean bit = bitSet.get(i);
			write(bit);
		}
	}
	
	/**
	 * Completa el ultimo byte del <code>OutputStream</code> asociado
	 * segun el <code>ByteCompletionMode</code> recibido en el
	 * constructor. Si el mismo es distinto de <code>NONE</code>,
	 * agrega un byte mas al OutputStream.
	 * @throws IOException si ocurre algun error al escribir
	 */
	private void completeByte() throws IOException {
		byte bytesToFill = (byte)(8 - bitsInLastByte);
		if (bytesToFill == 8 && completionMode == ByteCompletionMode.NONE) {
			return;
		}
		
		for (byte i = 0; i < bytesToFill; i++) {
			switch(completionMode) {
			case ONE_AND_TRAILING_ZEROS:
				write(i == 0);
				break;
			case ZERO_AND_TRAILING_ONES:
				write(i != 0);
				break;
			case NONE:
				write(false);
				break;
			}
		}
	}
}
